/*
 *  linux/fs/buffer.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 *  'buffer.c' implements the buffer-cache functions. Race-conditions have
 * been avoided by NEVER letting a interrupt change a buffer (except for the
 * data, of course), but instead letting the caller do it. NOTE! As interrupts
 * can wake up a caller, some cli-sti sequences are needed to check for
 * sleep-on-calls. These should be extremely quick, though (I hope).
 * 
 * buffer.c’用于实现缓冲区高速缓存功能。通过不让中断处理过程改变缓冲区，而是让调用者来执行，
 * 避免了竞争条件（当然除改变数据以外）。注意！由于中断可以唤醒一个调用者，
 * 因此就需要开关中断指令（cli-sti)序列来检测由于调用而睡眠。但需要非常地快 （我希望是这样）。
 */

/*
 * NOTE! There is one discordant note here: checking floppies for
 * disk change. This is where it fits best, I think, as it should
 * invalidate changed floppy-disk-caches.
 */

#include <stdarg.h>
 
#include <linux/config.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/system.h>
#include <asm/io.h>

extern int end; // 由连接程序ld 生成的位于程序未端的变量。
extern void put_super(int);
extern void invalidate_inodes(int);

struct buffer_head * start_buffer = (struct buffer_head *) &end;  // 内存布局中，高速缓冲区的起始地址
struct buffer_head * hash_table[NR_HASH];		// hash表，共NR_HASH=307项（每一项都是一个双向链表）
static struct buffer_head * free_list;			// 空闲链表
static struct task_struct * buffer_wait = NULL;	// 空闲缓冲块的等待队列(全局变量)
int NR_BUFFERS = 0;		// 系统含有缓冲块个数

// 等待指定缓冲块解锁
// 如果指定的缓冲块 bh 已经上锁就让进程不可中断地睡眠在该缓冲块的等待队列 b_wait中。
// 在缓冲块解锁时，其等待队列上的所有进程将被唤醒。
// 使用 sleep_on(进入睡眠状态的进程需要用 wakeup(明确地唤醒）。
static inline void wait_on_buffer(struct buffer_head * bh)
{
	/*
	虽然是在关闭中断（cli）之后去睡眠的，但这样做并不会影响在其他进程上下文中响应中断。
    因为每个进程都在自己的 TSS段中保存了标志寄存器 EFLAGS 的值，所以在进程切换时 CPU 中当前 EFLAGS 的值也随之改变。
	*/
	cli();
	while (bh->b_lock)				// 如果己被上锁，则进程进入睡眠，等待其解锁
		sleep_on(&bh->b_wait);
	sti();
}

// 设备数据同步（系统调用）。
// 同步内存高速缓冲中数据到设备中（所有设备）。
int sys_sync(void)
{
	int i;
	struct buffer_head * bh;
	
	/*
	首先调用 i节点同步函数，把内存 i节点表中所有修改过的i节点写入高速缓冲中。
	然后扫描所有高速缓冲区，对已被修改的缓冲块产生写盘请求，将缓冲中数据写入盘中，做到高速缓冲中的数据与设备中的同步。
	*/
	sync_inodes();		/* write out inodes into buffers */
	bh = start_buffer;
	for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
		wait_on_buffer(bh);			// 等待缓冲区解锁（如果己上锁的话）。
		if (bh->b_dirt)
			ll_rw_block(WRITE,bh);	// 产生写设备块请求项
	}
	return 0;
}


// 对指定设备进行高速缓冲数据与设备上数据的同步操作。
// 该函数首先搜索高速缓冲区中所有缓冲块。对于指定设备 dev 的缓冲块，若其数据已被修改过就写入盘中（同步操作）。
// 然后把内存中 i节点表数据写入高速缓冲中。之后再对指定设备 dev 执行一次与上述相同的写盘操作。
int sync_dev(int dev)
{
	int i;
	struct buffer_head * bh;

	bh = start_buffer;
	for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
		if (bh->b_dev != dev)
			continue;
		wait_on_buffer(bh);
		// 因为在我们睡眠期间该缓冲块有可能已被释放或者被挪作它用，
		// 所以在继续执行前需要再次判断一下该缓冲块是否还是指定设备的缓冲块，
		if (bh->b_dev == dev && bh->b_dirt)
			ll_rw_block(WRITE,bh);
	}
	sync_inodes();
	bh = start_buffer;
	for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
		if (bh->b_dev != dev)
			continue;
		wait_on_buffer(bh);
		if (bh->b_dev == dev && bh->b_dirt)
			ll_rw_block(WRITE,bh);
	}
	return 0;
}

// 使指定设备在高速缓冲区中的数据无效。
static void inline invalidate_buffers(int dev)
{
	int i;
	struct buffer_head * bh;

	bh = start_buffer;
	// 扫描高速缓冲区中所有缓冲块。对指定设备的缓冲块复位其有效(更新)标志和己修改标志
	for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
		if (bh->b_dev != dev)
			continue;
		wait_on_buffer(bh);
		if (bh->b_dev == dev)
			bh->b_uptodate = bh->b_dirt = 0;
	}
}

/*
 * This routine checks whether a floppy has been changed, and
 * invalidates all buffer-cache-entries in that case. This
 * is a relatively slow routine, so we have to try to minimize using
 * it. Thus it is called only upon a 'mount' or 'open'. This
 * is the best way of combining speed and utility, I think.
 * People changing diskettes in the middle of an operation deserve
 * to loose :-)
 *
 * NOTE! Although currently this is only for floppies, the idea is
 * that any additional removable block-device will use this routine,
 * and that mount/open needn't know that floppies/whatever are
 * special.
 * 
 * 该子程序检查一个软盘是否已被更换，如果已经更换就使高速缓冲中与该软驱对应的所有缓冲区无效。
 * 该子程序相对来说较慢，所以我们要尽量少使用它。
 * 所以仅在执行’mount’或’open’时才调用它。我想这是将速度和实用性相结合的最好方法。
 * 若在操作过程中更换软盘，就会导致数据的丢失。这是谷由自取®。
 * 
 * 注意！尽管目前该子程序仅用于软盘，以后任何可移动介质的块设备都将使用该程序，
 * mount/open 操作不需要知道是软盘还是其他什么特殊介质。
 */

// 检查磁盘是否更换，如果已更换就使对应高速缓冲区无效
void check_disk_change(int dev)
{
	int i;

	if (MAJOR(dev) != 2)
		return;
	if (!floppy_change(dev & 0x03))
		return;
	/*
	软盘已经更换，所以释放对应设备的i节点位图和逻辑块位图所占的高速缓冲区；
	并使该设备的i节点和数据块信息所占踞的高速缓冲块无效。
	*/
	for (i=0 ; i<NR_SUPER ; i++)
		if (super_block[i].s_dev == dev)
			put_super(super_block[i].s_dev);
	invalidate_inodes(dev);
	invalidate_buffers(dev);
}

// 下面两行代码是 hash（散列）函数定义和 hash 表项的计算宏。
/*
hash 表的主要作用是减少查找比较元素所花费的时间。
通过在元素的存储位置与关键字之间建立一个对应关系(hash 函数），我们就可以直接通过函数计算立刻查询到指定的元素。
建立hash 函数的指导条件主要是尽量确保散列到任何数组项的概率基本相等。建立函数的方法有多种，这里 Linux 0.11 主要采用了关键字除留余数法。
因为我们寻找的缓冲块有两个条件， 即设备号 dev 和缓冲块号 block，因此设计的 hash 函数肯定需要包含这两个关键值。
这里两个关键字的异或操作只是计算关键值的一种方法。再对关键值进行 MOD 运算就可以保证函数所计算得到的值都处于函数数组项范围内。
*/
// hash函数（取余）
#define _hashfn(dev,block) (((unsigned)(dev^block))%NR_HASH)
// 获取hash表中的表项（hash队列的头）
#define hash(dev,block) hash_table[_hashfn(dev,block)] 

// 从 hash队列 和 空闲缓冲队列 中移走缓冲块。
// hash 队列是双向链表结构，空闲缓冲块队列是双向循环链表结构
static inline void remove_from_queues(struct buffer_head * bh)
{
/* remove from hash-queue */
	// 如果当前的bh有next，则将next的prev设置为自己的prev（将自己的孩子的父亲设置为爷爷）
	// 将（当前项的）下一项的前一项指向当前项的前一项
	if (bh->b_next)
		bh->b_next->b_prev = bh->b_prev;
	// 同理，将父亲的孩子设置为孙子
	// // 将（当前项的）前一项的下一项指向当前项的下一项
	if (bh->b_prev)
		bh->b_prev->b_next = bh->b_next;
	// 如果该缓冲区是该队列的头一个块，则让 hash 表的对应项指向本队列中的下一个缓冲区。
	if (hash(bh->b_dev,bh->b_blocknr) == bh)
		hash(bh->b_dev,bh->b_blocknr) = bh->b_next;
/* remove from free list */
	// 空闲缓冲块队列是双向循环链表结构，正常情况下不会出现prev和next为NULL的情况
	if (!(bh->b_prev_free) || !(bh->b_next_free))
		panic("Free block list corrupted"); 
	// 将（当前项的）前一项的下一项指向当前项的下一项
	bh->b_prev_free->b_next_free = bh->b_next_free;
	// 将（当前项的）下一项的前一项指向当前项的前一项
	bh->b_next_free->b_prev_free = bh->b_prev_free;
	// 如果空闲链表头指向本缓冲区，则让其指向下一缓冲区
	if (free_list == bh)
		free_list = bh->b_next_free;
}

// 将缓冲块插入空闲链表尾部，同时放入 hash表 队列中
static inline void insert_into_queues(struct buffer_head * bh)
{
/* put at end of free list 放在空闲链表末尾处 */
	bh->b_next_free = free_list;				// “最为空闲的”缓冲块
	bh->b_prev_free = free_list->b_prev_free; 	// “最近刚使用的”缓冲块
	// 使双向链表循环
	free_list->b_prev_free->b_next_free = bh;
	free_list->b_prev_free = bh;
/* put the buffer in new hash-queue if it has a device 如果该缓冲块对应一个设备，则将其插入新 hash 队列头中 */
	bh->b_prev = NULL;
	bh->b_next = NULL;
	if (!bh->b_dev)
		return;
	bh->b_next = hash(bh->b_dev,bh->b_blocknr); // 请注意当 hash 表某项第1次插入项时，hash()计算值肯定为 NULL，即空的hash表项，因此此时第161 行上得到的 bh->b next 肯定是NULL，所以第163 行上应该在 bh-)b next 不为NULL 时才能给b prev 赋 bh 值。即第163 行前应该增加判断 “if (bh->b next)
	hash(bh->b_dev,bh->b_blocknr) = bh;
	// 此处应添加“if(bh->b_next)”判断，即：
	/*
	if(bh->b next)
		bh->b_next->b_prev = bh;
	*/
	bh->b_next->b_prev = bh;
}

// 利用 hash 表在高速缓冲中寻找给定设备和指定盘块号的缓冲区块。（在hash表中找到一个bh，其dev和block等于指定值）
// 返回值：如果找到，则返回bh；否则返回 NULL。
// 与get_hash_table的区别：
// 		get_hash_table会对bh的引用计数+1
// 		find_buffer只是寻找，不会做任何操作。
static struct buffer_head * find_buffer(int dev, int block)
{		
	struct buffer_head * tmp;

	// 对hash表项 链表中的所有bh进行遍历，检查是否有指定的buffer
	for (tmp = hash(dev,block) ; tmp != NULL ; tmp = tmp->b_next)
		if (tmp->b_dev==dev && tmp->b_blocknr==block)
			return tmp;
	return NULL;
}

/*
 * Why like this, I hear you say... The reason is race-conditions.
 * As we don't lock buffers (unless we are readint them, that is),
 * something might happen to it while we sleep (ie a read-error
 * will force it bad). This shouldn't really happen currently, but
 * the code is ready.
 * 代码为什么会是这样子的？我听见你问...原因是竞争条件。
 * 由于我们没有对缓冲块上锁（除非我们正在读取它们中的数据〉，
 * 那么当我们（进程）睡眼时缓冲块可能会发生一些问题（例如一个读错误将导致该缓冲块出错）。
 * 目前这种情况实际上是不会发生的，但处理的代码已经准备好了。
 */

// 利用 hash 表在高速缓冲区中寻找指定的缓冲块。若找到则对该缓冲块引用计数+1，并返回bh指针；否则返回NULL
// block - 盘块号 （是逻辑块号，注意不是数据逻辑块号）
struct buffer_head * get_hash_table(int dev, int block)
{
	struct buffer_head * bh;

	for (;;) {
		if (!(bh=find_buffer(dev,block)))
			return NULL;

		bh->b_count++;		// 对该缓冲块增加引用计数
		wait_on_buffer(bh);  // 等待锁释放（如果已被上锁）
		// 由于经过了睡眠状态， 因此有必要再验证该缓冲块的正确性，并返回缓冲块头指针。
		if (bh->b_dev == dev && bh->b_blocknr == block)
			return bh;
		// 如果在睡眠时该缓冲块所属的设备号或块号发生了改变，则撤消对它的引用计数，重新寻找。
		bh->b_count--;
	}
}

/*
 * Ok, this is getblk, and it isn't very clear, again to hinder
 * race-conditions. Most of the code is seldom used, (ie repeating),
 * so it should be much more efficient than it looks.
 *
 * The algoritm is changed: hopefully better, and an elusive bug removed.
 * 
 * OK，下面是 getblk 函数，该函数的逻辑并不是很清晰，同样也是因为要考虑竟争条件问题。
 * 其中大部分代码很少用到，（例如重复操作语句），因此它应该比看上去的样子有效得多。
 * 
 * 算法已经作了改变：希望能更好，而且一个难以琢磨的错误已经去除。
 */

// 修改-锁定权重（糟糕值）
// 用于同时判断缓冲区的修改标志和锁定标志，并且定义修改标志的权重要比锁定标志大。
// 高比特位：b_dirt；低比特位：b_lock
#define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock)

// 获取逻辑块对应的缓冲块（bh）。（注意：此时，该缓冲块中可能没有有效数据。如果没有可以通过bread()读取设置中的数据）
// 获取（或新建）高速缓冲(即hash表)中指定（设备号和块号）的缓冲块bh，返回bh指针。引用数+1
// 该函数会使用指定设备号dev和逻辑块块号block从 hash表（高速缓冲区）中获取一个 buffer_head：
// 	如果获取到，直接返回缓冲区头指针；
// 	如果获取不到，则新建并返回，同时会插入到hash表、重新插入到free_list中。
// 实现：
// 		如果指定块己经在高速缓冲中，则返回对应缓冲区头指针退出；
// 		如果不在，就需要在高速缓冲中设置一个对应设备号和块号的bh新项，返回相应缓冲区头指针。
struct buffer_head * getblk(int dev,int block)
{
	struct buffer_head * tmp, * bh;

repeat:
	// 搜索 hash 表，如果指定块己经在高速缓冲中（即在hash表中存在），则返回对应缓冲区头指针，退出。
	if ((bh = get_hash_table(dev,block)))
		return bh;

	// 如果不在haash表中，则扫描空闲数据块链表，寻找空闲缓冲区（空闲缓冲区中不一定是空闲未使用的，会根据糟糕值选择出最优的bh）。
	// 首先让 tmp 指向空闲链表的第一个空闲缓冲区头。
	tmp = free_list;
	do {
		// 如果该缓冲区正被使用（引用计数不等于0，表示被其他进程提前获取了），则继续扫描下一项。
		if (tmp->b_count)
			continue;
		/*
		对于 b_count=0 的块， 即高速缓冲中当前没有引用的块不一定就是干净的（b_dirt=0）或没有锁定的(b_lock=0）。
		因此，我们还是需要继续下面的判断和选择。
		例如
		当一个任务改写过一块内容后就释放了， 于是该块 b_count = 0，但b_lock 不等于0；具体场景如下：
		当一个任务执行 breada() 预读几个块时，只要 ll_rw_block(）命令发出后，它就会递减b_count；
		但此时实际上硬盘访问操作可能还在进行， 因此此时 b_lock=1，但 b_count=0。
		*/
		// 如果缓冲头指针 bh 为空，或者 tmp 所指缓冲头的标志(修改、锁定）权重小于 bh 头标志的权重，
		// 则让 bh 指向 tmp 缓冲块头。
		if (!bh || BADNESS(tmp)<BADNESS(bh)) {
			bh = tmp;
			// 如果该 tmp 缓冲块头表明缓冲块既没有修改也没有锁定标志置位（糟糕值为0），则说明己为指定设备上的块取得对应的高速缓冲块，则退出循环。
			// 否则我们就继续执行本循环，看看能否找到一个 BADNESS()最小的缓冲快。
			if (!BADNESS(tmp))
				break;
		}
/* and repeat until we find something good */
	} while ((tmp = tmp->b_next_free) != free_list);

	// 如果循环检查发现所有缓冲块都正在被使用（所有缓冲块的头部引用计数都>0）中，则睡眠等待有空闲缓冲块可用。
	// 当有空闲缓冲块可用时本进程会被明确地唤醒。然后我们就跳转到函数开始处重新查找空闲缓冲块。
	if (!bh) {
		sleep_on(&buffer_wait);
		goto repeat;
	}

	// 执行到这里，说明我们己经找到了一个比较适合的空闲缓冲块了。
	// 于是先等待该缓冲区解锁 （如果已被上锁的话）。如果在我们睡眠阶段该缓冲区又被其他任务使用的话，只好重复上述寻找过程。
	wait_on_buffer(bh);
	if (bh->b_count)
		goto repeat;

	// 如果该缓冲区己被修改，则将数据写盘（脏数据落盘），并睡眠再次等待缓冲区解锁。
	// 同样地，若该缓冲区又被其他任务使用的话，只好再重复上述寻找过程。
	while (bh->b_dirt) {
		sync_dev(bh->b_dev);
		wait_on_buffer(bh);
		if (bh->b_count)
			goto repeat;
	}
/* NOTE!! While we slept waiting for this block, somebody else might */
/* already have added "this" block to the cache. check it */
/*
注意！！当进程为了等待该缓冲块而睡眠时，其他进程可能已经将该缓冲块加入进高速缓冲中，所以我们也要对此进行检查。
*/

	// 在高速缓冲 hash 表中检查指定设备和块的缓冲块是否乘我们睡眠之机己经被加入进去。
	// 如果是的话，就再次重复上述寻找过程。
	if (find_buffer(dev,block))
		goto repeat;
/* OK, FINALLY we know that this buffer is the only one of it's kind, */
/* and that it's unused (b_count=0), unlocked (b_lock=0), and clean */
/* 
	OK，最终我们知道该缓冲块是指定参数的唯一一块，而且目前还没有被占用(b_count=0)，
	也未被上锁(b_lock=0)，并且是干净的（b_dirt=0，末被修改的）
*/
	// 于是让我们占用此缓冲块。置引用计数为1，复位修改标志和有效(更新） 标志。
	bh->b_count=1;
	bh->b_dirt=0;
	bh->b_uptodate=0;
	// 从 hash 队列和空闲块链表中移出该缓冲区头，让该缓冲区用于指定设备和其上的指定块。
	// 然后根据此新的设备号和块号重新插入空闲链表和 hash 队列新位置处。并最终返回缓冲头指针。
	remove_from_queues(bh);   // 因为dev和block值发生变化，所以在hash表、空闲链表中的位置发生了变化，需要先移除再添加
	bh->b_dev=dev;
	bh->b_blocknr=block;
	insert_into_queues(bh);
	return bh;
}

// 释放指定缓冲块引用（b_count减1）。
// 说明：brelse()只是释放对该块的1次引用；free_block()是真正的释放块
void brelse(struct buffer_head * buf)
{
	if (!buf)
		return;
	// 等待该缓冲块解锁。然后引用计数递减1，并明确地唤醒等待空闲缓冲块的进程。
	wait_on_buffer(buf);
	if (!(buf->b_count--))
		panic("Trying to free free buffer");
	wake_up(&buffer_wait);
}

/*
 * bread() reads a specified block and returns the buffer that contains
 * it. It returns NULL if the block was unreadable.
 * 从设备上读取指定的数据块并返回含有数据的缓冲区。如果指定的块不存在则返回 NULL。
 */

// 读取指定磁盘设备块，获取块对应的buffer_head
// 实现：
// 首先根据指定的设备号 dev 和 盘块号 block，在高速缓冲区中申请一块缓冲块。
// 如果该缓冲块中己经包含有有效的数据，就直接返回该缓冲块指针，
// 否则，就从设备中读取指定的数据块到该缓冲块中并返回缓冲块指针。
// 如果读取设备失败，则返回NULL
struct buffer_head * bread(int dev,int block)
{
	struct buffer_head * bh;

	// 在高速缓冲区中申请一块缓冲块。如果返回值是 NULL，则表示内核出错，停机。
	if (!(bh=getblk(dev,block)))
		panic("bread: getblk returned NULL\n");
	// 然后我们判断其中是否己有可用数据。如果该缓冲块中数据是有效的（己更新的）可以直接使用， 则返回。
	if (bh->b_uptodate)
		return bh;
	// 否则我们就调用底层块设备读写 ll_rw_block(） 函数，产生读设备块请求。
	ll_rw_block(READ,bh);
	// 然后等待指定数据块被读入，并等待缓冲区解锁。
	wait_on_buffer(bh);
	// 在睡眠醒来之后，如果该缓冲区已更新，则返回缓冲区头指针，退出。
	if (bh->b_uptodate)
		return bh;
	// 否则表明读设备操作失败，于是释放该缓冲区，返回NULL，退出。
	brelse(bh);
	return NULL;
}

// 复制内存块
// 从from 地址复制一块（1024字节）数据到to位置。
#define COPYBLK(from,to) \
__asm__("cld\n\t" \
	"rep\n\t" \
	"movsl\n\t" \
	::"c" (BLOCK_SIZE/4),"S" (from),"D" (to) \
	)

/*
 * bread_page reads four buffers into memory at the desired address. It's
 * a function of its own, as there is some speed to be got by reading them
 * all at the same time, not waiting for one to be read, and then another
 * etc.
 * 
 * bread_page 一次读四个缓冲块数据读到内存指定的地址处。
 * 它是一个完整的函数， 因为同时读取四块可以获得速度上的好处，不用等着读一块，再读一块了。
 */

// 读设备上一个页面（4个缓冲块）的内容到指定内存地址处。（先将数据从设备读到高速缓冲区，然后再拷贝到内存指定处）
// 参数:
// 		address 是保存页面数据的地址；
// 		dev 是指定的设备号；
// 		b[4]是含有4个设备数据块号的数组。
// 该函数仅用于 mm/memory.c 文件的 do_no_page(）函数中（第386 行)。
void bread_page(unsigned long address,int dev,int b[4])
{
	struct buffer_head * bh[4];
	int i;

	// 将从块设备上读取4个指定块号的数据到4个缓冲块上
	for (i=0 ; i<4 ; i++)
		if (b[i]) { // 若块号有效
			if ((bh[i] = getblk(dev,b[i])))
				if (!bh[i]->b_uptodate)
					ll_rw_block(READ,bh[i]);
		} else
			bh[i] = NULL;
	// 随后将4个缓冲块上的内容顺序复制到指定地址处
	for (i=0 ; i<4 ; i++,address += BLOCK_SIZE)
		if (bh[i]) {
			wait_on_buffer(bh[i]);
			if (bh[i]->b_uptodate)
				COPYBLK((unsigned long) bh[i]->b_data,address);
			// 数据拷贝到内存指定处后，释放缓冲块（buffer块只是临时使用）
			brelse(bh[i]);
		}
}

/*
 * Ok, breada can be used as bread, but additionally to mark other
 * blocks for reading as well. End the argument list with a negative
 * number.
 * 
 * OK， breada 可以象 bread一样使用，但会另外预读一些块。该函数参数列表需要使用一个负数来表明参数列表的结束。
 */

// 从指定设备读取指定的一些块。
// 函数参数个数可变，是一系列指定的块号。成功时返回第1块的缓冲块头指针，否则返回 NULL。
struct buffer_head * breada(int dev,int first, ...)
{
	va_list args;
	struct buffer_head * bh, *tmp;
	
	// 首先取可变参数表中第1个参数（块号）。
	va_start(args,first);
	// 接着从高速缓冲区中取指定设备和块号的缓冲块。
	if (!(bh=getblk(dev,first)))
		panic("bread: getblk returned NULL\n");
	// 如果该缓冲块数据无效（更新标志未置位），则发出读设备数据块请求。
	if (!bh->b_uptodate)
		ll_rw_block(READ,bh);
	// 然后顺序取可变参数表中其他预读块号，并作与上面同样处理，但不引用。
	/* 
		注意，336行上有一个 bug。其中的 bh 应该是 tmp。这个 bug 直到在0.96版的内核代码中才被纠正过来。
		因为这里是预读随后的数据块，只需读进高速缓冲区但并不是马上就使用，
		所以第337 行语句需要将其引用计数递减释放掉该块（因为 getblk()函数会增加引用计数值）。
	*/
	while ((first=va_arg(args,int))>=0) {
		tmp=getblk(dev,first);
		if (tmp) {
			if (!tmp->b_uptodate)
				ll_rw_block(READA,bh);  // bug: bh 应该是 tmp。
			tmp->b_count--;  // getblk()会将引用增加，此处将其恢复
		}
	}
	// 此时可变参数表中所有参数处理完毕。
	va_end(args);
	// 于是等待第1个缓冲区解锁（如果已被上锁）。
	wait_on_buffer(bh);
	// 在等待退出之后，如果缓冲区中数据仍然有效，则返回缓冲区头指针退出。
	if (bh->b_uptodate)
		return bh;
	// 否则释放该缓冲区返回 NULL，退出。
	brelse(bh);
	return (NULL);
}

void buffer_init(long buffer_end)
{
	struct buffer_head * h = start_buffer;
	void * b;
	int i;

	// 如果缓冲区高端等于1Mb，则由于从64GKB-1MB 被显示内存和BIOS 占用，
	// 因此实际可用缓冲区内存高端应该是640KB。否则内存高端一定大于1MB.
	if (buffer_end == 1<<20)
		b = (void *) (640*1024);
	else
		b = (void *) buffer_end;
	// 这段代码用于初始化缓冲区，建立空闲缓冲区环链表，并获取系统中缓冲块的数目。
	/*
	参考：图12-16 高速缓冲区的初始化
	操作的过程是从缓冲区高端开始划分1K大小的缓冲块，与此同时在缓冲区低端开始建立描述该缓冲块的结构 buffer_head（缓冲头），并将这些buffer_head组成双向链表。
	h是指向缓冲头结构的指针，而h+1 是指向内存地址连续的下一个缓冲头地址，也可以说是指向h 缓冲头的未端处。
	是为了保证有足够长度的内存来存储一个缓冲头结构，需要b所指向的内存块地址>= h 缓冲头的末端，也即要>=h+1。	
	*/
	while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {
		h->b_dev = 0;
		h->b_dirt = 0;
		h->b_count = 0;
		h->b_lock = 0;
		h->b_uptodate = 0;
		h->b_wait = NULL;
		h->b_next = NULL;
		h->b_prev = NULL;
		h->b_data = (char *) b;  // 在初始化时（此时），就将所有的bh的b_data设置完成，后续只是使用
		h->b_prev_free = h-1;
		h->b_next_free = h+1;
		h++;
		NR_BUFFERS++;
		// 跳过640k-1M的内存空间，这段内存地址是显存和bios rom使用
		if (b == (void *) 0x100000)
			b = (void *) 0xA0000;
	}
	h--;  // 让h指向最后一个有效缓冲头。
	free_list = start_buffer;    //让空闲链表头指向头一个缓冲区头。
	free_list->b_prev_free = h;  //链表头的b_prev_free 指向前一项（即最后一项）
	h->b_next_free = free_list;  // h 的下一项指针指向第一项，形成一个环链。
	// 初始化hash 表（哈希表、散列表），置表中所有的指针为NULL。
	for (i=0;i<NR_HASH;i++)
		hash_table[i]=NULL;
}	

 
